package com.rengwuxian.coursecoroutines._4_flow

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.flow.timeout
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

/**
 * timeout()/sample/debounce()
 * 超时 / 时间窗口 / timeout() / sample() / debounce()
 *
 * 1.timeout(): 会从flow的collect开始计时，在超时时间内，如果Flow还没有结束并且还没有发出下一条数据，就抛一个TimeoutCancellationException.
 *   比如：聊天软件，会显示对方正在输入的提示，如果持续多少s后，没有，就消失，就适合这种场景。
 *   flow1.timeout(1.seconds).collect { println("1: $it") }
 *   抛的异常可以被捕获，根据实际场景来用。
 *   TimeoutCancellationException是CancellationException的子类，如果没有被捕获，外部协程就走取消流程。
 *   注意：如果Flow结束了，整个协程也就结束了，最后一个数据不会被发送出来。
 *
 * 2.sample(): 每段时间内发送过来的所有数据只能把其中一条留下（最新的一条），作为一个样例，而其他数据就被丢弃掉。
 *   flow1.sample(1.seconds).collect { println("2: $it") }
 *   每隔1s取一条数据，第一次取数据是1s的时间窗口点，不是0s，故UI使用时需要注意首次延时效果。
 *   适合定时刷新的场景。
 *
 * 3.debounce(): 数据到debounce()这里先不发送，等待一个设定的时长，
 *   如果等待的期间又有新的数据到达，那么那条等待中的数据就直接丢弃，转而开始压住这条新的收到数据，并且启动新一轮的等待。
 *   如果等待时间内没有新数据到来，就将此条数据发出。（超时的时候才会发送数据）
 *   如果很快有新数据到来，debounce()可能会一直压住，一直不发。
 *   应用场景：
 *     1.搜索框，输入完后，等待一段时间，如果用户没有新的输入，再去搜索。
 *       Sug搜。
 *
 * 4.界面开发，防止多次点击的去抖动。不适合debounce()。协程的Flow没有这样的操作符，可以自定义。
 *   代码如下：
 *   fun <T> Flow<T>.throttle(timeWindow: Duration): Flow<T> = flow {
 *     var lastTime = 0L
 *     collect {
 *       if (System.currentTimeMillis() - lastTime > timeWindow.inWholeMilliseconds) {
 *         emit(it)
 *         lastTime = System.currentTimeMillis()
 *       }
 *     }
 *   }
 */
@OptIn(FlowPreview::class)
fun main() = runBlocking<Unit> {
  val scope = CoroutineScope(EmptyCoroutineContext)
  val flow1 = flow {
    emit(0)
    delay(500)
    emit(1)
    delay(800)
    emit(2)
    delay(900)
    emit(3)
    delay(1000)
  }
  scope.launch {
//    flow1.timeout(1.seconds).collect { println("1: $it") }
//    flow1.sample(1.seconds).collect { println("2: $it") }
    flow1.debounce(1.seconds).collect { println("3: $it") }
  }
  delay(10000)
}

fun <T> Flow<T>.throttle(timeWindow: Duration): Flow<T> = flow {
  var lastTime = 0L
  collect {
    if (System.currentTimeMillis() - lastTime > timeWindow.inWholeMilliseconds) {
      emit(it)
      lastTime = System.currentTimeMillis()
    }
  }
}